fix: propagate null bitmap in evaluate_map_to_struct#2419
fix: propagate null bitmap in evaluate_map_to_struct#2419DrakeLin merged 5 commits intodelta-io:mainfrom
Conversation
When the input MapArray has null rows (e.g. for remove/metadata/protocol actions where the parent `add` struct is null), evaluate_map_to_struct appends null values to all child field builders but creates the output StructArray with no null buffer (None). This causes Arrow validation to reject the array with "Found unmasked nulls for non-nullable StructArray field" when any output field is declared non-nullable. This manifests during checkpoint creation for partitioned tables with NOT NULL partition columns and delta.checkpoint.writeStatsAsStruct=true. The COALESCE(partitionValues_parsed, MAP_TO_STRUCT(partitionValues)) expression evaluates MAP_TO_STRUCT for all rows including those where the map is null, triggering the validation error. The fix propagates the input MapArray's null bitmap to the output StructArray, matching the pattern established in PR delta-io#1645 for nested transform expressions. Co-authored-by: Isaac
|
@momcilomrk-db -- please add unit tests for this change. Clearly we have a gap in coverage. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2419 +/- ##
========================================
Coverage 88.34% 88.34%
========================================
Files 171 171
Lines 56696 56983 +287
Branches 56696 56983 +287
========================================
+ Hits 50087 50341 +254
- Misses 4695 4714 +19
- Partials 1914 1928 +14 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
dengsh12
left a comment
There was a problem hiding this comment.
LGTM, but please add tests for the case you mentioned in description, thanks!
| arrow_fields.into(), | ||
| output_columns, | ||
| None, | ||
| map_array.nulls().cloned(), |
There was a problem hiding this comment.
Please add a comment.
- Before: This code was
Noneand it was so obviously correct that it wasn't worth having a comment about why it is None. - Now: The code is
map_array.nulls().cloned()and, since I don't see any comment, apparently it is obvious why this should bemap_array.nulls().cloned().
Yet -- this was a bug. Clearly it is not obvious.
Please add a detailed comment explaining why, what happens when it is present, what happens if it was NOT present, etc.
We need to remind ourselves in the future (and perhaps in other similar areas of the code) why this value MUST be set.
@dengsh12 and @DrakeLin -- Remember to raise the bar on PR reviews and aim for excellent code clariity
There was a problem hiding this comment.
Good point! But in this special case ... I feel like reserving Let's add a comment stating the corner case, and in the future claude would know this corner case existsmap_array.nulls().cloned() is the straightforward way(extract things from the MapArray and retain the nulls), perhaps we should add comments if we set it to None(uncommon since dropping the nullabilities)?
There was a problem hiding this comment.
I agree I think it makes sense to add detailed comments now more than before since Claude can pick it up and not create the same bug in the future.
For example I asked the claude to look at the previous PRs (to find when the bug was introduced), and it also found this related issue #1645 which seems to be the same None pattern. If there was a comment maybe it would pick it up in this case as well.
|
Please do not merge until comment above is addressed |
Add three tests covering the null bitmap fix in evaluate_map_to_struct: 1. test_map_to_struct_propagates_null_bitmap_for_non_nullable_fields: Direct test -- null map rows with non-nullable output fields. Verifies the output struct is null where the input map is null, preventing "Found unmasked nulls for non-nullable StructArray field" errors. 2. test_coalesce_map_to_struct_with_null_map_non_nullable_fields: Simulates the checkpoint expression COALESCE(partitionValues_parsed, MAP_TO_STRUCT(partitionValues)) with a null map row and NOT NULL partition column. This is the exact pattern that fails during checkpoint creation. 3. test_map_to_struct_all_null_maps_with_non_nullable_fields: Edge case where every map row is null. Verifies the output struct is entirely null despite non-nullable child fields. Co-authored-by: Isaac
|
Addressed the comments. I added the tests you can review it @DrakeLin |
DrakeLin
left a comment
There was a problem hiding this comment.
Can we use rstest for the first 2 test functions?
#[rstest]
#[case::mixed_nulls(
vec![Some(vec![("k", "v")]), None, Some(vec![("k",
"x")])],
vec![true, false, true],
)]
#[case::all_nulls(
vec![None, None],
vec![false, false],
)]
fn test_map_to_struct_null_propagation_with_non_nullable_field
s(...)
|
|
||
| #[test] | ||
| fn test_map_to_struct_null_map_with_non_nullable_fields() { | ||
| use crate::arrow::array::{MapBuilder, StringBuilder}; |
There was a problem hiding this comment.
Hoist up to test module. Repeated in all tests.
There was a problem hiding this comment.
Thanks for the comments. Both are done. Previously I followed the pattern from the other Rust tests in that file. I removed use for other tests as well.
The rstest is a good approach I did it, thanks.
- Hoist MapBuilder and StringBuilder imports to module level (removes 6 inline use statements across existing and new tests) - Consolidate the two MAP_TO_STRUCT null propagation tests into a single parametrized rstest with mixed_nulls and all_nulls cases - Fix rustfmt formatting in test_coalesce_map_to_struct Co-authored-by: Isaac
What changes are proposed in this pull request?
evaluate_map_to_structcreates the outputStructArraywithNoneas the null buffer, even when the inputMapArrayhas null rows. This causes Arrow validation to reject the array with"Found unmasked nulls for non-nullable StructArray field"when any output field is non-nullable.This manifests during checkpoint creation for partitioned tables where:
NOT NULLin the table schemadelta.checkpoint.writeStatsAsStruct = trueaddis nullThe
COALESCE(partitionValues_parsed, MAP_TO_STRUCT(partitionValues))expression evaluatesMAP_TO_STRUCTfor all rows. For rows where the map is null, child builders get null values appended but the output struct has no null mask to cover them.Context:
MapToStructwas introduced in #1895 and used forpartitionValues_parsedin #1932.How was this change tested?